﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using System.ComponentModel;
using Brain.Skinning;
using Brain;

namespace Brain_Skinning
{
    [ContentProcessor(DisplayName = "Model - Brain Skinning")]
    public class SkinnedModelProcessor : ContentProcessor<NodeContent, SkinnedModelContent>
    {
        [DisplayName("Scale"),
        Description("Uniformily scales the Model"),
        DefaultValue(1f)]
        public float UniformScale
        {
            get { return scale; }
            set { scale = value; }
        }
        private float scale = 1;

        [DisplayName("Model Processor"),
        Description("The processor that will be used to process the model's data"),
        DefaultValue("ModelProcessor")]
        public string ModelProcessor
        {
            get { return modelProcessor; }
            set { modelProcessor = value; }
        }
        private string modelProcessor = "ModelProcessor";

        [DisplayName("Rotation"),
        Description("Rotates the Model around the X, Y and Z axis"),
        DefaultValue(typeof(Vector3), "0, 0, 0")]
        public Vector3 Rotation { get; set; }

        [DisplayName("Position Offset"),
        Description("Modifies the Model Position"),
        DefaultValue(typeof(Vector3), "0, 0, 0")]
        public Vector3 PositionOffset { get; set; }

        public override SkinnedModelContent Process(NodeContent input,
            ContentProcessorContext context)
        {
            // Find the Mesh Skeleton
            BoneContent skeleton = MeshHelper.FindSkeleton(input);

            if (skeleton == null)
            {
                throw new InvalidContentException("Mesh skeleton was not found by XNA!");
            }

            BakeTransforms(input, skeleton);
            // Apply user chosen Rotation and Scale
            TransformModel(input);

            /////////////////////////////////////////
            //---------- Read Bone Data -----------//
            /////////////////////////////////////////

            // Get the Bones
            IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);

            // Makes an array with all the Bones
            SkinnedBone[] Bones = new SkinnedBone[bones.Count];

            Matrix[] bindPoses = new Matrix[bones.Count];
            // Copy the Bone Content data to our Runtime class, SkinnedBone
            for (int i = 0; i < bones.Count; i++)
            {
                BoneContent bone = bones[i];
                Bones[i] = new SkinnedBone(Matrix.Invert(bone.AbsoluteTransform),
                    bones.IndexOf(bone.Parent as BoneContent), bone.Name);
                bindPoses[i] = bone.Transform;
            }

            /////////////////////////////////////////
            //------- Read Animation Clips --------//
            /////////////////////////////////////////
            // Process the Animations
            Dictionary<string, SkinnedAnimationClip> animationClips = ProcessAnimations(skeleton.Animations, bones);

            /////////////////////////////////////////
            //------- Process Model ---------------//
            /////////////////////////////////////////
            OpaqueDataDictionary dictionary = new OpaqueDataDictionary();
            if (ModelProcessor.ToLower() == "modelprocessor")
            {
                dictionary.Add("DefaultEffect", MaterialProcessorDefaultEffect.SkinnedEffect);
            }
            ModelContent model =
                context.Convert<NodeContent, ModelContent>(input, ModelProcessor, dictionary);

            SkinnedModelContent skModel = new SkinnedModelContent(model,
                Bones, animationClips);
            skModel.BindPoses = bindPoses;

            return skModel;
        }

        /// <summary>
        /// Apply user chosen transforms to the mesh
        /// </summary>
        /// <param name="input"></param>
        private void TransformModel(NodeContent input)
        {
            Matrix mat = Matrix.Identity;
            bool found = false;
            if (UniformScale != 1 &&
                UniformScale != 0)
            {
                found = true;
                mat = Matrix.CreateScale(UniformScale);
            }
            if (Rotation != Vector3.Zero)
            {
                found = true;
                mat *= MatrixUtil.DegreesVectorToMatrix(Rotation);
            }
            if (PositionOffset != Vector3.Zero)
            {
                found = true;
                mat *= Matrix.CreateTranslation(PositionOffset);
            }
            if (found)
            {
                MeshHelper.TransformScene(input, mat);
            }
        }

        /// <summary>
        /// Converts an intermediate format content pipeline AnimationContentDictionary
        /// object to our runtime AnimationClip format.
        /// </summary>
        private static Dictionary<string, SkinnedAnimationClip> ProcessAnimations(
            AnimationContentDictionary animations, IList<BoneContent> bones)
        {
            // Build up a table mapping bone names to indices.
            Dictionary<string, int> boneMap = new Dictionary<string, int>();

            for (int i = 0; i < bones.Count; i++)
            {
                string boneName = bones[i].Name;

                if (!string.IsNullOrEmpty(boneName))
                    boneMap.Add(boneName, i);
            }

            // Convert each animation in turn.
            Dictionary<string, SkinnedAnimationClip> animationClips = new Dictionary<string, SkinnedAnimationClip>();

            foreach (KeyValuePair<string, AnimationContent> animation in animations)
            {
                SkinnedAnimationClip processed = ProcessAnimation(animation.Value, boneMap);

                animationClips.Add(animation.Key, processed);
            }

            if (animationClips.Count == 0)
            {
                return null;
                throw new InvalidContentException(
                            "Input file does not contain any animations. Currently you cannot import a Skinned Model without animations (CURRENTLY)");
            }

            return animationClips;
        }

        /// <summary>
        /// Converts an intermediate format content pipeline AnimationContent
        /// object to our runtime AnimationClip format.
        /// </summary>
        private static SkinnedAnimationClip ProcessAnimation(AnimationContent animation,
                                              Dictionary<string, int> boneMap)
        {
            Dictionary<int, List<Frame_Skinned>> keyframes = new Dictionary<int, List<Frame_Skinned>>();
            List<string> boneNames = new List<string>();

            // For each input animation channel.
            foreach (KeyValuePair<string, AnimationChannel> channel in
                animation.Channels)
            {
                // Look up what bone this channel is controlling.
                int boneIndex;
                boneNames.Add(channel.Key);

                if (!boneMap.TryGetValue(channel.Key, out boneIndex))
                {
                    throw new InvalidContentException(string.Format(
                        "Found animation for bone '{0}', " +
                        "which is not part of the skeleton.", channel.Key));
                }

                if (!keyframes.ContainsKey(boneIndex))
                {
                    //BoneKeyFrameCollection col = new BoneKeyFrameCollection(boneIndex);
                    //col.Bone = boneIndex;
                    List<Frame_Skinned> skin = new List<Frame_Skinned>();
                    keyframes.Add(boneIndex, skin);
                }


                List<Frame_Skinned> list = keyframes[boneIndex];
                // Convert the keyframe data.
                foreach (AnimationKeyframe keyframe in channel.Value)
                {
                    Matrix trans = keyframe.Transform;
                    int milli = (int)(keyframe.Time.TotalMilliseconds);

                    list.Add(new Frame_Skinned(milli, trans));
                }
            }

            List<BoneKeyFrameCollection> bones = new List<BoneKeyFrameCollection>();

            int index = 0;
            foreach (KeyValuePair<int, List<Frame_Skinned>> valuePair in keyframes)
            {
                int boneIndex = valuePair.Key;
                List<Frame_Skinned> keyFrames = valuePair.Value;
                // Sort the merged keyframes by time.
                keyFrames.Sort(CompareKeyframeTimes);

                BoneKeyFrameCollection col = new BoneKeyFrameCollection(boneIndex, keyFrames.ToArray());
                //col.BoneName = boneNames[index];
                bones.Add(col);

                index++;
            }

            Dictionary<int, BoneKeyFrameCollection> allBones = new Dictionary<int, BoneKeyFrameCollection>();
            for (int i = 0; i < bones.Count; i++)
            {
                BoneKeyFrameCollection bon = bones[i];
                allBones.Add(bon.BoneIndex, bon);
            }

            if (keyframes.Count == 0)
                throw new InvalidContentException("Animation has no keyframes.");

            if (animation.Duration <= TimeSpan.Zero)
                throw new InvalidContentException("Animation has a zero duration.");

            bones.Sort(CompareBoneFrame);

            return new SkinnedAnimationClip((int)(animation.Duration.TotalMilliseconds), allBones);
        }

        static int CompareBoneFrame(BoneKeyFrameCollection a, BoneKeyFrameCollection b)
        {
            return a.BoneIndex.CompareTo(b.BoneIndex);
        }

        /// <summary>
        /// Comparison function for sorting keyframes into ascending time order.
        /// </summary>
        static int CompareKeyframeTimes(Frame_Skinned a, Frame_Skinned b)
        {
            return a.Time.CompareTo(b.Time);
        }

        private static void BakeTransforms(NodeContent node,
            BoneContent skeleton)
        {
            foreach (NodeContent child in node.Children)
            {
                if (child != skeleton)
                {
                    MeshHelper.TransformScene(child, child.Transform);

                    child.Transform = Matrix.Identity;

                    BakeTransforms(child, skeleton);
                }
            }
        }
    }
}